<?php
/**
 * lsys orm
* @author     Lonely <shan.liu@msn.com>
* @copyright  (c) 2017 Lonely <shan.liu@msn.com>
* @copyright  (c) 2007-2012 Kohana Team
* @license    http://www.apache.org/licenses/LICENSE-2.0
* @license    http://kohanaframework.org/license
*/
namespace LSYS;
use LSYS\Database;
use LSYS\ORM\Entity;
use function LSYS\ORM\__;
use LSYS\ORM\Result;
use LSYS\ORM\Transaction;
abstract class ORM implements ConfigFinder {
	use \LSYS\ConfigFinder\ConfigShare;
	/**
	 * Table name
	 *
	 * @var string
	 */
	protected $_table_name;
	/**
	 * Table primary key
	 *
	 * @var string
	 */
	protected $_primary_key = 'id';
	/**
	 * Table columns
	 *
	 * @var array
	 */
	protected $_table_columns = array ();
	/**
	 * Table defaults
	 *
	 * @var array
	 */
	protected $_table_defaults = array ();
	/**
	 * "Has one" relationships
	 * 对方有字段存本身主键
	 * "model"=>"对方模型名",
	 * "foreign_key"=>"对方存本身主键的字段名"
	 * @var array
	 */
	protected $_has_one = array ();
	/**
	 * "Belongs to" relationships
	 * 本身存对方主键
	 * "model"=>"对方模型名",
	 * "foreign_key"=>"本身存对方键的字段名",
	 * @var array
	 */
	protected $_belongs_to = array ();

	/**
	 * "Has many" relationships
	 * 对方存本身主键 ,但对方有多条记录
	 * 方式1
 	 *			"model"=>"对方模型名",
 	 *			"foreign_key"=>"对方存本身主键的字段名",
	 * 方式2
 	 *		"model"=>"对方模型名",
 	 *			"through"=>"关系表名",
 	 *			"far_key"=>"关系表存对方主键的字段名",
 	 *			"foreign_key"=>"关系表存本身主键的字段名",
	 * @var array
	 */
	protected $_has_many = array ();
	/**
	 * Initialization storage for ORM models
	 *
	 * @var array
	 */
	protected static $_init_cache = array ();
	/**
	 * Validation object created before saving/updating
	 *
	 * @var Validation
	 */
	protected $_validation = NULL;
	/**
	 * @var bool
	 */
	protected $_saved = FALSE;
	/**
	 * Foreign key suffix
	 *
	 * @var string
	 */
	protected $_foreign_key_suffix = '.id';
	/**
	 * Model name
	 *
	 * @var string
	 */
	protected $_object_name = null;

	/**
	 * Auto-update columns for updates
	 *
	 * @var string
	 */
	protected $_updated_column = NULL;

	/**
	 * Auto-update columns for creation
	 * "column" => "add_time",
	 *	"format" => true
	 * @var string
	 */
	protected $_created_column = NULL;

	/**
	 * Database Object
	 *
	 * @var Database
	 */
	protected $_db = NULL;
	/**
	 * Database config key
	 *
	 * @var string
	 */
	protected $_db_config= NULL;

	/**
	 * Database methods pending
	 *
	 * @var array
	 */
	protected $_db_pending = array ();

	/**
	 * Reset builder
	 *
	 * @var bool
	 */
	protected $_db_reset = TRUE;
	protected $_related = array ();
	/**
	 * @var string
	 */
	protected $_entity_name = null;
	/**
	 * @var Entity
	 */
	protected $_entity = null;
	/**
	 * @var Entity
	 */
	protected $_original_entity = null;

	/**
	 * Constructs a new model and loads a record if given
	 *
	 * @param mixed $id
	 *        	Parameter for find or object to load
	 */
	public function __construct($id=NULL) {
		$this->_init();
		$this->_init_defaults();
		$this->_init_find($id);
	}
	/**
	 * Filter definitions for Validation
	 * 'nickname' => array (
	 *		array ('trim'),
	 *		array ('strip_tags')
	 *	),
	 * @return array
	 */
	public function filters() {
		return array ();
	}
	/**
	 * Label definitions for Validation
	 *
	 * @return array
	 */
	public function labels() {
		return array ();
	}
	/**
	 * Rule definitions for Validation
	 * 'nickname' => array (
	 *		array('not_empty'),
	 *		array('max_length', array(':value', 32)),
	 *	)
	 * @return array
	 */
	public function rules() {
		return array ();
	}
	protected function _init_defaults(){
		if(!isset(ORM::$_init_cache[$this->_object_name]['_table_defaults'])){
			$defs=get_class_vars($this->_entity_name);
			if (is_array($defs)){
				foreach ($this->_table_columns as $key){
					if(!array_key_exists($key, $defs))unset($defs[$key]);
				}
			}else $defs=array();
			ORM::$_init_cache[$this->_object_name]['_table_defaults']=$defs;
		}
		$this->_table_defaults=ORM::$_init_cache[$this->_object_name]['_table_defaults'];
	}
	protected function _init_find($id){
		if ($id!==null) $this->where_pk($id)->find();
	}
	protected function _init(){
		$cls=get_called_class ();
		$entity_name=substr($cls,strrpos($cls,"\\")+4);
		// Set the object name and plural name
		if ($this->_object_name === null) {
			$this->_object_name = strtolower($entity_name);
		}
		// Check if this model has already been initialized
		if (! isset(ORM::$_init_cache[$this->_object_name])) {
			$init = array (
				'_belongs_to' => array (),
				'_has_one' => array (),
				'_has_many' => array ()
			);
			if (! is_object ( $this->_db )) {
				// Get database instance
				if ($this->_db_config)$config= self::find_config($this->_db_config);
				else $config=null;
				$init ['_db'] = Database::instance ($config);
			}
			if (empty ( $this->_table_name )) {
				// Table name is the same as the object name
				$init ['_table_name'] = $this->_object_name;
			}
			
			if (empty ( $this->_entity_name )) {
				$cls=isset($cls)?$cls:get_called_class ();
				$pos=strrpos($cls,"\\");
				$init ['_entity_name'] = substr($cls,0,$pos===false?0:$pos+1)."Entity".$entity_name;
			}
			$defaults = array ();
			foreach ( $this->_belongs_to as $alias => $details ) {
				$defaults ['foreign_key'] = $alias . $this->_foreign_key_suffix;
				$init ['_belongs_to'] [$alias] = array_merge ( $defaults, $details );
			}
			foreach ( $this->_has_one as $alias => $details ) {
				$defaults ['foreign_key'] = $this->_object_name . $this->_foreign_key_suffix;
				$init ['_has_one'] [$alias] = array_merge ( $defaults, $details );
			}
			foreach ( $this->_has_many as $alias => $details ) {
				$defaults ['foreign_key'] = $this->_object_name . $this->_foreign_key_suffix;
				$defaults ['through'] = NULL;
				if (! isset ( $details ['far_key'] )) {
					$defaults ['far_key'] = $alias . $this->_foreign_key_suffix;
				}
				$init ['_has_many'] [$alias] = array_merge ( $defaults, $details );
			}
			ORM::$_init_cache [$this->_object_name] = $init;
		}else $init=ORM::$_init_cache[$this->_object_name];
		// Assign initialized properties to the current object
		foreach ( $init as $property => $value ) {
			$this->{$property} = $value;
		}
	}
	/**
	 * entity op
	 * @param Entity $entity
	 * @return \LSYS\ORM\Entity|$this
	 */
	public function entity(Entity $entity=null) {
		if ($entity===null){
			if ($this->_entity === null) {
				$this->_entity = new $this->_entity_name ($this);
			}
			return $this->_entity;
		}
		if ($this->_entity!==$entity){//compare not update...
			if ($this->_original_entity!==null) unset($this->_original_entity);
			$this->_original_entity = clone $entity;
			$this->_saved = FALSE;
		}
		$this->_entity = $entity;
		return $this;
	}
	/**
	 * Initializes Validation rules, and labels
	 *
	 * @return void
	 */
	protected function _validation() {
		// Build the Validation object with its rules
		if($this->_validation!==null)unset($this->_validation);

		$object = array ();
		$entity=$this->entity();
		foreach ( $this->_table_columns as $key ) {
			$entity->{$key}=$object[$key]=$this->run_filter($key, $entity->{$key});
		}
		$this->_validation = Validation::factory ($object)
		->bind ( ':model', $this )
		->bind ( ':original', $this->_original_entity );
		foreach ( $this->rules () as $field => $rules ) {
			$this->_validation->rules ( $field, $rules );
		}

		// Use column names by default for labels
		$columns = $this->_table_columns;

		// Merge user-defined labels
		$labels = array_merge ( array_combine ( $columns, $columns ), $this->labels () );

		foreach ( $labels as $field => $label ) {
			$this->_validation->label ( $field, $label );
		}
	}
	/**
	 * Reloads the current object from the database.
	 *
	 * @chainable
	 *
	 * @return $this
	 */
	public function reload() {
		$primary_key = $this->_entity->pk();
		// Replace the object and reset the object status
		$this->_related = array ();
		// Only reload the object if we have one to reload
		if ($this->entity()->loaded()){
			$pk=$this->entity()->pk();
			$this->entity()->clear();
			$this->where ( $this->primary_key(), '=', $pk)
			->find ();
		}
		return $this;
	}
	/**
	 * Check whether the model data has been modified.
	 * If $field is specified, checks whether that field was modified.
	 *
	 * @param string $field
	 *        	field to check for changes
	 * @return bool Whether or not the field has changed
	 */
	public function changed($field = NULL) {
		return count($this->_entity->diff($this->_original_entity,$field))>0;
	}
	/**
	 * Finds and loads a single database row into the object.
	 * @return $this
	 */
	public function find() {
		$this->limit ( 1 );
		$field=$this->_build_field();
		$sql = $this->_build_select ($field);
		$res = $this->_db->query (Database::DQL, $sql );
		if ($this->entity()->loaded()){
			$res->set_fetch_mode ( Database::FETCH_CLASS, $this->_entity_name, array (
					$this
			) );
			$entity=$res->current();
			if ($entity===null){
				unset($this->_original_entity);
				$this->_entity->clear();
			}else $this->entity($entity);
		}else{
			$entity=$res->set_fetch_mode ( Database::FETCH_INTO,$this->entity())->current();
			if ($entity===null) unset($this->_original_entity);
			else $this->_original_entity=clone $this->_entity;
		}
		$this->reset();
		return $this;
	}
	/**
	 * Finds multiple database rows and returns an iterator of the rows found.
	 *
	 * @return Result
	 */
	public function find_all() {
		$field=$this->_build_field ();
		$sql = $this->_build_select ($field);
		$result = $this->_db->query (Database::DQL, $sql );
		$result->set_fetch_mode ( Database::FETCH_CLASS, $this->_entity_name, array (
				$this
		) );
		$this->reset();
		return new Result($result);
	}
	/**
	 * Handles getting of column
	 * Override this method to add custom get behavior
	 *
	 * @param string $column
	 *        	Column name
	 * @throws Exception
	 * @return ORM
	 */
	public function __get($column) {
		if (isset ( $this->_related [$column] )) {
			// Return related model that has already been fetched
			return $this->_related [$column];
		} elseif (isset ( $this->_belongs_to [$column] )) {
			$model = $this->_related ( $column );
				
			// Use this model's column and foreign model's primary key
			$col = $model->_primary_key;
				
			$val = $this->entity()->{$this->_belongs_to [$column] ['foreign_key']};
				
			if ($val !== NULL) {
				$model->where ( $col, '=', $val )->find ();
			}
				
			return $this->_related [$column] = $model;
		} elseif (isset ( $this->_has_one [$column] )) {
			$model = $this->_related ( $column );
				
			// Use this model's primary key value and foreign model's column
			$col = $this->_has_one [$column] ['foreign_key'];
			$val = $this->entity()->pk ();
			
			if ($val !== NULL) {
				$model->where ( $col, '=', $val )->find ();
			}
			
			return $this->_related [$column] = $model;
		} elseif (isset ( $this->_has_many [$column] )) {
			$model = $this->_has_many [$column] ['model'];
			if (!is_subclass_of($model, __CLASS__))
				throw new \LSYS\ORM\Exception(__("model :model not :class child class",array("model"=>$model,"class"=>__CLASS__)));
				/**
				 * @var ORM $model
				 */
				$model = new $model;
					
				if (isset ( $this->_has_many [$column] ['through'] )) {
					// Grab has_many "through" relationship table
					$through = $this->_has_many [$column] ['through'];

					// Join on through model's target foreign key (far_key) and target model's primary key
					$join_col1 = $through . '.' . $this->_has_many [$column] ['far_key'];
					$join_col2 = $model->_table_name . '.' . $model->_primary_key;

					$model->join ( array (
							$through,
							$through
					) )->on ( $join_col1, '=', $join_col2 );
					// Through table's source foreign key (foreign_key) should be this model's primary key
					$col = $through . '.' . $this->_has_many [$column] ['foreign_key'];
				} else {
					// Simple has_many relationship, search where target model's foreign key is this model's primary key
					$col = $this->_has_many [$column] ['foreign_key'];
				}
				$val = $this->entity()->pk ();
				return $model->where ( $col, '=', $val );
		} else {
			throw new \LSYS\ORM\Exception ( __('The :model property does not exist in the :class class',array("model"=>$model,"class"=>get_called_class())) );
		}
	}
	/**
	 * Filters a value for a specific column
	 *
	 * @param string $field
	 *        	The column name
	 * @param string $value
	 *        	The value to filter
	 * @return string
	 */
	protected function run_filter($field, $value) {
		$filters = $this->filters ();

		// Get the filters for this column
		$wildcards = empty ( $filters [TRUE] ) ? array () : $filters [TRUE];

		// Merge in the wildcards
		$filters = empty ( $filters [$field] ) ? $wildcards : array_merge ( $wildcards, $filters [$field] );

		// Bind the field name and model so they can be used in the filter method
		$_bound = array (
				':field' => $field,
				':model' => $this
		);

		foreach ( $filters as $array ) {
			// Value needs to be bound inside the loop so we are always using the
			// version that was modified by the filters that already ran
			$_bound [':value'] = $value;

			// Filters are defined as array($filter, $params)
			$filter = $array [0];
			$params = isset($array[1])?$array[1]:array (
					':value'
			) ;

			foreach ( $params as $key => $param ) {
				if (is_string ( $param ) and array_key_exists ( $param, $_bound )) {
					// Replace with bound value
					$params [$key] = $_bound [$param];
				}
			}

			if (is_array ( $filter ) or ! is_string ( $filter )) {
				// This is either a callback as an array or a lambda
				$value = call_user_func_array ( $filter, $params );
			} elseif (strpos ( $filter, '::' ) === FALSE) {
				// Use a function call
				$function = new \ReflectionFunction ( $filter );

				// Call $function($this[$field], $param, ...) with Reflection
				$value = $function->invokeArgs ( $params );
			} else {
				// Split the class and method of the rule
				list ( $class, $method ) = explode ( '::', $filter, 2 );

				// Use a static method call
				$method = new \ReflectionMethod ( $class, $method );

				// Call $Class::$method($this[$field], $param, ...) with Reflection
				$value = $method->invokeArgs ( NULL, $params );
			}
		}

		return $value;
	}

	
	/**
	 * Insert a new object to the database
	 *
	 * @param Validation $Validation
	 *        	Validation object
	 * @throws  \LSYS\Database\Exception|\LSYS\ORM\Exception
	 * @return $this
	 */
	public function create() {
		if ($this->entity()->loaded()){
			throw new \LSYS\ORM\Exception ( __('Cannot create :object model because it is already loaded.',array("object"=>$this->_object_name)) );
		}
		$data = array ();
		
		foreach ($this->table_defaults() as $k=>$v){
			if(isset($this->_entity->{$k})&&$this->_entity->{$k}!==$v) $data [$k] = $this->_entity->{$k};
		}
		
		if (is_array ( $this->_created_column )) {
			// Fill the created column
			$column = $this->_created_column ['column'];
			$format = $this->_created_column ['format'];
			$this->_entity->{$column} = $data [$column] = ($format === TRUE) ? time () : date ( $format );
		}
		
		$this->_db_insert($this->_table_name, $data);

		if (! array_key_exists ( $this->_primary_key, $data )) {
			// Load the insert id as the primary key if it was left out
			$this->_entity->{$this->_primary_key} = $this->_db->insert_id();
		}

		// Object is now loaded and saved
		$this->_saved = TRUE;

		unset($this->_original_entity);
		$this->_original_entity = clone $this->_entity;

		$this->_db->set_query(Database::QUERY_MASTER);

		return $this;
	}
	/**
	 * Updates a single record or multiple records
	 *
	 * @chainable
	 *
	 * @param Validation $Validation
	 *        	Validation object
	 * @throws  \LSYS\Database\Exception|\LSYS\ORM\Exception
	 * @return $this
	 */
	public function update() {
		if ( ! $this->entity()->loaded()){
			throw new \LSYS\ORM\Exception(__('Cannot update :object model because it is not loaded.',array("object"=>$this->_object_name)));
		}
		
		$data = array ();

		foreach ($this->table_defaults() as $k=>$v){
			if(isset($this->_entity->{$k})
			&&$this->_original_entity->{$k}!==$this->_entity->{$k}
			) $data [$k] = $this->_entity->{$k};
		}

		if (count($data)==0){
			$this->_saved = TRUE;
			return $this;
		}

		if (is_array ( $this->_updated_column )) {
			// Fill the updated column
			$column = $this->_updated_column ['column'];
			$format = $this->_updated_column ['format'];

			$this->_entity->{$column} = $data [$column] = ($format === TRUE) ? time () : date ( $format );
		}


		// Use primary key value
		$db = $this->_db;
		$where = $this->_primary_key . '=' . $db->quote($this->_entity->pk());
		$this->_db_update($this->_table_name, $data,$where);
		if (isset ( $data [$this->_primary_key] )) {
			// Primary key was changed, reflect it
			$this->_entity->{$this->_primary_key} = $data [$this->_primary_key];
		}
		// Object has been saved
		$this->_saved = TRUE;


		unset($this->_original_entity);
		$this->_original_entity = clone $this->_entity;

		$this->_db->set_query(Database::QUERY_MASTER);
		return $this;
	}

	/**
	 * Updates or Creates the record depending on loaded()
	 *
	 * @chainable
	 *
	 * @param Validation $Validation
	 *        	Validation object
	 * @throws \LSYS\Database\Exception|\LSYS\ORM\Exception
	 * @return $this
	 */
	public function save() {
		return $this->entity()->loaded() ? $this->update ( ) : $this->create ( );
	}

	/**
	 * Deletes a single record while ignoring relationships.
	 *
	 * @chainable
	 *
	 * @throws  \LSYS\Database\Exception|\LSYS\ORM\Exception
	 * @return $this
	 */
	public function delete() {
		if (! $this->entity()->loaded ())
			throw new \LSYS\ORM\Exception ( __('Cannot delete :object model because it is not loaded.',array("object"=>$this->_object_name )) );
		// Use primary key value
		$id = $this->entity()->pk ();
		$where = $this->_primary_key . '=' . $this->_db->quote($id);
		$this->_db_delete ( $this->_table_name, $where );
		$this->_entity->clear();
		unset($this->_original_entity);
		$this->_db->set_query(Database::QUERY_MASTER);
		return $this->reset();
	}
	/**
	 * Tests if this object has a relationship to a different model,
	 * or an array of different models.
	 * When providing far keys, the number
	 * of relations must equal the number of keys.
	 *
	 *
	 * // Check if $model has the login role
	 * $model->has('roles', ORM::factory('role', array('name' => 'login')));
	 * // Check for the login role if you know the roles.id is 5
	 * $model->has('roles', 5);
	 * // Check for all of the following roles
	 * $model->has('roles', array(1, 2, 3, 4));
	 * // Check if $model has any roles
	 * $model->has('roles')
	 *
	 * @param string $alias
	 *        	Alias of the has_many "through" relationship
	 * @param mixed $far_keys
	 *        	Related model, primary key, or an array of primary keys
	 * @return boolean
	 */
	public function has($alias, $far_keys = NULL) {
		$count = $this->count_relations ( $alias, $far_keys );
		if ($far_keys === NULL) {
			return ( bool ) $count;
		} else {
			return $count === count ( $far_keys );
		}
	}
	/**
	 * Returns the number of relationships
	 *
	 *     // Counts the number of times the login role is attached to $model
	 *     $model->count_relations('roles', ORM::factory('role', array('name' => 'login')));
	 *     // Counts the number of times role 5 is attached to $model
	 *     $model->count_relations('roles', 5);
	 *     // Counts the number of times any of roles 1, 2, 3, or 4 are attached to
	 *     // $model
	 *     $model->count_relations('roles', array(1, 2, 3, 4));
	 *     // Counts the number roles attached to $model
	 *     $model->count_relations('roles')
	 *
	 * @param  string  $alias    Alias of the has_many "through" relationship
	 * @param  mixed   $far_keys Related model, primary key, or an array of primary keys
	 * @return integer
	 */
	public function count_relations($alias, $far_keys = NULL)
	{
		if ($far_keys === NULL)
		{
			$table=$this->_db->quote_table($this->_has_many[$alias]['through']);
			$column=$this->_db->quote_column($this->_has_many[$alias]['foreign_key']);
			$pk=$this->_db->quote($this->entity()->pk());
			$sql=" SELECT COUNT(*) as records_found FROM {$table} WHERE {$column} = {$pk}";
			$result=$this->_db->query(Database::DQL, $sql);
			return (int)$result->get('records_found');
		}
		$far_keys = ($far_keys instanceof ORM) ? $far_keys->entity()->pk() : $far_keys;
		// We need an array to simplify the logic
		$far_keys = (array) $far_keys;
		// Nothing to check if the model isn't loaded or we don't have any far_keys
		if ( ! $far_keys OR ! $this->_loaded)
			return 0;
		$table=$this->_db->quote_table($this->_has_many[$alias]['through']);
		$column1=$this->_db->quote_column($this->_has_many[$alias]['foreign_key']);
		$column2=$this->_db->quote_column($this->_has_many[$alias]['foreign_key']);
		$pk=$this->_db->quote($this->entity()->pk());
		$val=$this->_db->quote($far_keys);
		$sql=" SELECT COUNT(*) as records_found FROM {$table} WHERE {$column1} = {$pk} and {$column2} in {$val} ";
		$result=$this->_db->query(Database::DQL, $sql);
		return (int)$result->get('records_found');
	}
	/**
	 * Tests if this object has a relationship to a different model,
	 * or an array of different models.
	 * When providing far keys, this function
	 * only checks that at least one of the relationships is satisfied.
	 *
	 * // Check if $model has the login role
	 * $model->has('roles', ORM::factory('role', array('name' => 'login')));
	 * // Check for the login role if you know the roles.id is 5
	 * $model->has('roles', 5);
	 * // Check for any of the following roles
	 * $model->has('roles', array(1, 2, 3, 4));
	 * // Check if $model has any roles
	 * $model->has('roles')
	 *
	 * @param string $alias
	 *        	Alias of the has_many "through" relationship
	 * @param mixed $far_keys
	 *        	Related model, primary key, or an array of primary keys
	 * @return boolean
	 */
	public function has_any($alias, $far_keys = NULL) {
		return ( bool ) $this->count_relations ( $alias, $far_keys );
	}
	/**
	 /**
	 * Adds a new relationship to between this model and another.
	 *
	 * // Add the login role using a model instance
	 * $model->add('roles', ORM::factory('role', array('name' => 'login')));
	 * // Add the login role if you know the roles.id is 5
	 * $model->add('roles', 5);
	 * // Add multiple roles (for example, from checkboxes on a form)
	 * $model->add('roles', array(1, 2, 3, 4));
	 *
	 * @param string $alias
	 *        	Alias of the has_many "through" relationship
	 * @param mixed $far_keys
	 *        	Related model, primary key, or an array of primary keys
	 * @return $this
	 */
	public function add($alias, $far_keys) {
		$far_keys = ($far_keys instanceof ORM) ? $far_keys->entity()->pk () : $far_keys;

		$foreign_key = $this->entity()->pk ();

		foreach ( ( array ) $far_keys as $key ) {
			$record = array (
				$this->_has_many [$alias] ['foreign_key'] => $foreign_key,
				$this->_has_many [$alias] ['far_key'] => $key
			);
			$this->_db_insert($this->_has_many [$alias] ['through'], $record);
		}
		return $this;
	}

	/**
	 * Removes a relationship between this model and another.
	 *
	 * // Remove a role using a model instance
	 * $model->remove('roles', ORM::factory('role', array('name' => 'login')));
	 * // Remove the role knowing the primary key
	 * $model->remove('roles', 5);
	 * // Remove multiple roles (for example, from checkboxes on a form)
	 * $model->remove('roles', array(1, 2, 3, 4));
	 * // Remove all related roles
	 * $model->remove('roles');
	 *
	 * @param string $alias
	 *        	Alias of the has_many "through" relationship
	 * @param mixed $far_keys
	 *        	Related model, primary key, or an array of primary keys
	 * @return $this
	 */
	public function remove($alias, $far_keys = NULL) {
		$far_keys = ($far_keys instanceof ORM) ? $far_keys->pk () : $far_keys;

		$where = $this->_db_column ( $this->_has_many [$alias] ['foreign_key'] ) . '=' . $this->pk ();

		if ($far_keys !== NULL) {
			$where .= ' and ' . $this->_db_column ( $this->_has_many [$alias] ['far_key'] ) . ' IN (' . implode ( ",", ( array ) $far_keys ) . ")";
		}
		$this->_db_delete ( $this->_has_many [$alias] ['through'], $where);
		return $this;
	}
	/**
	 * Count the number of records in the table.
	 *
	 * @return integer
	 */
	public function count_all() {
		$sql = $this->_build_select (" count(*) as total ");
		$this->reset ();
		$row = $this->_db->query (Database::DQL, $sql );
		$cur=$row->current();
		return isset($cur['total'])?$cur['total']:0;
	}
	/**
	 * get db pending
	 * @return arraay
	 */
	public function db_pending() {
		return $this->_db_pending;
	}
	/**
	 * Clears query builder.
	 * Passing FALSE is useful to keep the existing
	 * query conditions for another query.
	 *
	 * @param bool $next
	 *        	Pass FALSE to avoid resetting on the next call
	 * @return $this
	 */
	public function reset($next = TRUE) {
		if ($next and $this->_db_reset) {
			$this->_db_pending = array ();
		}
		// Reset on the next call?
		$this->_db_reset = $next;
		return $this;
	}
	public function primary_key() {
		return $this->_primary_key;
	}
	public function object_name() {
		return $this->_object_name;
	}
	public function entity_name(){
		return $this->_entity_name;
	}
	public function saved() {
		if($this->_saved===false) return $this->_saved;
		$this->_saved=$this->_saved&&!$this->changed();
		return $this->_saved;
	}
	public function table_name() {
		return $this->_table_name;
	}
	public function table_columns() {
		return $this->_table_columns;
	}
	public function table_defaults() {
		return $this->_table_defaults;
	}
	public function has_one() {
		return $this->_has_one;
	}
	public function belongs_to() {
		return $this->_belongs_to;
	}
	public function has_many() {
		return $this->_has_many;
	}
	public function created_column() {
		return $this->_created_column;
	}
	public function updated_column() {
		return $this->_updated_column;
	}
	public function validation() {
		if (! isset ( $this->_validation )) {
			// Initialize the Validation object
			$this->_validation ();
		}
		return $this->_validation;
	}
	/**
	 * Validates the current model's data
	 *
	 * @param Validation $extra_validation
	 *        	Validation object
	 * @throws DomainException
	 * @return $this
	 */
	public function check(Validation $extra_validation = NULL) {
		// Determine if any external Validation failed
		$extra_errors = ($extra_validation and ! $extra_validation->check ());
		$this->_validation();
		$array = $this->_validation;
		if ($array->check () === FALSE or $extra_errors) {
			$message=$this->validation()->errors(true);
			if ($extra_errors)$message+=$extra_validation->errors(true);
			throw (new \LSYS\ORM\Exception (__('validateion fail'),\LSYS\ORM\Exception::VALIDATION_CODE))->set_validation_error($message);
		}
		return $this;
	}
	/**
	 * Returns an ORM model for the given one-one related alias
	 *
	 * @param string $alias
	 *        	Alias name
	 * @return ORM
	 */
	protected function _related($alias) {
		if (isset ( $this->_related [$alias] )) {
			return $this->_related [$alias];
		} elseif (isset ( $this->_has_one [$alias] )) {
			$model = $this->_has_one [$alias] ['model'];
			if (! is_subclass_of ( $model, __CLASS__ ))
				throw new \LSYS\ORM\Exception ( __("has_one column :alias model :model not extends ORM!",array("alias"=>$alias,"model"=>$model)) );
				return $this->_related [$alias] = new  $model;
		} elseif (isset ( $this->_belongs_to [$alias] )) {
			$model = $this->_belongs_to [$alias] ['model'];
			if (! is_subclass_of ( $model, __CLASS__ ))
				throw new \LSYS\ORM\Exception ( __("belongs_to column :alias model :model not extends ORM!",array("alias"=>$alias,"model"=>$model)) );
				return $this->_related [$alias] = new $model;
		} else {
			return FALSE;
		}
	}
	//db method...
	protected function _db_insert($table,array $record){
		$db=$this->_db;
		$table=$db->quote_table($table);
		$field=array();
		$data=array();
		foreach ($record as $key=>$value)
		{
			array_push($field, $this->_db_column($key));
			if (is_array($value))$value=implode(",", $value);
			array_push($data, $db->quote($value));
		}
		$str_field=implode(",",$field);
		$str_data='('.implode(",", $data).')';
		$sql=" INSERT INTO ".$table." (".$str_field.")VALUES {$str_data}";
		return $db->query ( Database::DML, $sql );
	}
	protected function _db_update($table,array $record,$where="1") {
		$db = $this->_db;
		$table = $db->quote_table ( $table );
		$sets = array ();
		foreach ( $record as $key => $value ) {
			$set = $this->_db_column( $key ) . " = " . $db->quote ( $value );
			array_push ( $sets, $set );
		}
		$str_set = implode ( ",", $sets );
		$sql = " UPDATE " . $table . " SET " . $str_set." where " . $where;
		return $db->query ( Database::DML, $sql );
	}
	protected function _db_delete($table,$where='1'){
		$db=$this->_db;
		$table=$db->quote_table($table);
		$sql=" DELETE FROM ".$table." where ".$where;
		return $db->query ( Database::DML, $sql );
	}
	/**
	 * 修复字段
	 *
	 * @param string $column
	 * @return string
	 */
	protected function _db_column($column) {
		return $this->_db->quote_column ( $column );
	}
	protected function _build_where_op($field, $op, $value) {
		$op = preg_replace ( "/\s+/", ' ', $op );
		switch ($op) {
			case 'in' :
			case 'not in' :
			case 'exists' :
			case 'not exists' :
				return $this->_db_column ( $field ) . ' ' . $op . " (" . $this->_db->quote ( $value ) . ") ";
				break;
			default :
				return $this->_db_column ( $field ) . ' ' . $op . " " . $this->_db->quote ( $value ) . " ";
		}
	}
	protected function _build_where_join($where_fragment, $op,&$where) {
		$swhere = trim ( $where );
		if (empty ( $swhere ) || substr ( $swhere, - 1, 1 ) == '(')
			$where .= ' ' . $where_fragment . ' ';
			else
			$where .= ' ' . $op . ' ' . $where_fragment . ' ';
	}
	/**
	 * 编译条件
	 *
	 * @return string
	 */
	protected function _build_where() {
		$where = ' ';
		foreach ( $this->_db_pending as $method ) {
			switch ($method ['name']) {
				case 'where' :
				case 'and_where' :
					$this->_build_where_join( call_user_func_array ( array($this,'_build_where_op'), $method ['args'] ), 'and' ,$where);
					break;
				case 'or_where' :
					$this->_build_where_join( call_user_func_array ( array($this,'_build_where_op'), $method ['args'] ), 'or' ,$where);
					break;
				case 'where_open' :
				case 'and_where_open' :
					$this->_build_where_join( '(', 'and' ,$where);
					break;
				case 'or_where_open' :
					$this->_build_where_join( '(', 'or' ,$where);
					break;
				case 'where_close' :
				case 'and_where_close' :
				case 'or_where_close' :
					$where .= ' ) ';
					break;
			}
		}
		return $where;
	}
	protected function _build_group_having_op($field, $op, $value){
		return $this->_db_column ( $field ) . ' ' . $op . " " . $this->_db->quote ( $value ) . " ";
	}
	protected function _build_group_having_join($having_fragment, $op,&$having){
		$shaving = trim ( $having );
		if (empty ( $shaving ) || substr ( $shaving, - 1, 1 ) == '(')
			$having .= ' ' . $having_fragment . ' ';
			else
				$having .= ' ' . $op . ' ' . $having_fragment . ' ';
	}
	/**
	 * 编译GROUP
	 *
	 * @return string
	 */
	protected function _build_group() {
		$db = $this->_db;
		$group = array ();
		$having = '';
		foreach ( $this->_db_pending as $method ) {
			switch ($method ['name']) {
				case 'group_by' :
					$column = $method ['args'];
					$column = $this->_db_column ( $column );
					$group [] = $column;
					break;
				case 'having' :
				case 'and_having' :
					$this->_build_group_having_join( call_user_func_array ( array($this,'_build_group_having_op'), $method ['args'] ), 'and' ,$having);
					break;
				case 'or_having' :
					$this->_build_group_having_join(  call_user_func_array ( array($this,'_build_group_having_op'), $method ['args'] ), 'or' ,$having);
					break;
				case 'and_having_open' :
				case 'having_open' :
					$this->_build_group_having_join( '(', 'and' ,$having);
					break;
				case 'or_having_open' :
					$this->_build_group_having_join( '(', 'or' ,$having);
					break;
				case 'and_having_close' :
				case 'having_close' :
				case 'or_having_close' :
					$having .= ' ) ';
					break;
			}
		}
		if (empty ( $group ))
			return '';
			if (! empty ( $having ))
				$having = 'HAVING ' . $having;
				return ' GROUP BY ' . implode ( ', ', $group ) . " " . $having;
	}
	/**
	 * 编译JOIN
	 *
	 * @return string
	 */
	protected function _build_join() {
		$sql = ' ';
		$on = false;
		$db = $this->_db;
		foreach ( $this->_db_pending as $method ) {
			switch ($method ['name']) {
				case 'join' :
					list ( $table, $type ) = $method ['args'];
					if (! empty ( $type ))
						$sql .= strtoupper ( $type ) . ' JOIN';
						else
							$sql .= 'JOIN';
							$table = $db->quote_table ( $table );
							$sql .= ' ' . $table;
							$on = false;
							break;
				case 'on' :
					list ( $c1, $op, $c2 ) = $method ['args'];
					if ($op) {
						$op = ' ' . strtoupper ( $op );
					}
					$j = '' . $this->_db_column ( $c1 ) . ' ' . $op . ' ' . $this->_db_column ( $c2 );
					if (! $on) {
						$sql .= ' ON (' . $j . ')';
					} else {
						$sql = rtrim ( $sql, ") " ) . ' and ' . $j . ')';
					}
					$on = true;
					break;
			}
		}
		return $sql;
	}
	protected function _build_order_op($field,$order){
		return ' '.$this->_fix_column($field).' '.addslashes($order);
	}
	/**
	 * 编译查询
	 *
	 * @return string
	 */
	protected function _build_select($field) {
		$db = $this->_db;
		$distinct = false;
		foreach ( $this->_db_pending as $method ) {
			switch ($method ['name']) {
				case 'distinct' :
					$distinct = ( bool ) $method ['args'] [0];
					break;
			}
		}
		
		$distinct=$distinct?" DISTINCT ":'';
		$table=$this->_db->quote_table($this->_table_name);
		$sql=" SELECT $distinct ".$field." FROM ".$table;
		$sql .= $this->_build_join ();

		$order = '';
		$limit = null;
		$offset = null;

		foreach ( $this->_db_pending as $method ) {
			switch ($method ['name']) {
				case 'order_by' :
					$_od = call_user_func_array(array($this,'_build_order_op'),$method['args']);
					$order ? ($order .= ' ,' . $_od) : ($order .= " order by " . $_od);
					break;
				case 'limit' :
					$limit = " limit " . $method ['args'] [0];
					break;
				case 'offset' :
					$offset = " offset " . $method ['args'] [0];
					break;
			}
		}
		if ($offset !== null && $limit === null) $limit = " limit " . PHP_INT_MAX;
		$where = $this->_build_where ();
		$where = empty ( trim ( $where ) ) ? ' ' : ' where ' . $where;
		$group = $this->_build_group ();
		return $sql . $where . $group . $order . $limit . $offset;
	}

	/**
	 * Returns an array of columns to include in the select query.
	 * This method
	 * can be overridden to change the default select behavior.
	 *
	 * @return array Columns to select
	 */
	protected function _build_field() {
		$db = $this->_db;
		$columns = array ();
		foreach ( $this->_table_columns as $column) {
			$columns [] = $column;
		}
		$_field=array();
		foreach ($columns as $value)
		{
			array_push($_field, $db->quote_column(array($this->_table_name.".".$value,$value)));
		}
		$field=implode(",", $_field);
		return $field;
	}

	/**
	 * set primary_key = $pk where
	 * @param string $pk
	 * @return $this
	 */
	public function where_pk($pk){
		$this->where($this->_table_name.".".$this->_primary_key, "=", $pk);
		return $this;
	}
	/**
	 * Alias of and_where()
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function where($column, $op, $value) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'where',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$op,
						$value
				)
		);
		return $this;
	}

	/**
	 * Creates a new "AND WHERE" condition for the query.
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function and_where($column, $op, $value) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_where',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$op,
						$value
				)
		);
		return $this;
	}

	/**
	 * Creates a new "OR WHERE" condition for the query.
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function or_where($column, $op, $value) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_where',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$op,
						$value
				)
		);

		return $this;
	}

	/**
	 * Alias of and_where_open()
	 *
	 * @return $this
	 */
	public function where_open() {
		return $this->and_where_open ();
	}

	/**
	 * Opens a new "AND WHERE (...)" grouping.
	 *
	 * @return $this
	 */
	public function and_where_open() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_where_open',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Opens a new "OR WHERE (...)" grouping.
	 *
	 * @return $this
	 */
	public function or_where_open() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_where_open',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Closes an open "AND WHERE (...)" grouping.
	 *
	 * @return $this
	 */
	public function where_close() {
		return $this->and_where_close ();
	}

	/**
	 * Closes an open "AND WHERE (...)" grouping.
	 *
	 * @return $this
	 */
	public function and_where_close() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_where_close',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Closes an open "OR WHERE (...)" grouping.
	 *
	 * @return $this
	 */
	public function or_where_close() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_where_close',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Applies sorting with "ORDER BY ..."
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $direction
	 *        	direction of sorting
	 * @return $this
	 */
	public function order_by($column, $direction = NULL) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'order_by',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$direction
				)
		);

		return $this;
	}

	/**
	 * Return up to "LIMIT ..." results
	 *
	 * @param integer $number
	 *        	maximum results to return
	 * @return $this
	 */
	public function limit($number) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'limit',
				'args' => array (
						intval ( $number )
				)
		);

		return $this;
	}

	/**
	 * Enables or disables selecting only unique columns using "SELECT DISTINCT"
	 *
	 * @param boolean $value
	 *        	enable or disable distinct columns
	 * @return $this
	 */
	public function distinct($value = true) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'distinct',
				'args' => array (
						$value
				)
		);

		return $this;
	}
	/**
	 * Start returning results after "OFFSET ..."
	 *
	 * @param integer $number
	 *        	starting result number
	 * @return $this
	 */
	public function offset($number) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'offset',
				'args' => array (
						intval ( $number )
				)
		);

		return $this;
	}

	/**
	 * Adds addition tables to "JOIN ...".
	 *
	 * @param mixed $table
	 *        	column name or array($column, $alias)
	 * @param string $type
	 *        	join type (LEFT, RIGHT, INNER, etc)
	 * @return $this
	 */
	public function join($table, $type = NULL) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'join',
				'args' => array (
						$table,
						$type
				)
		);

		return $this;
	}
	/**
	 * Adds "ON ..." conditions for the last created JOIN statement.
	 *
	 * @param mixed $c1
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $c2
	 *        	column name or array($column, $alias) or object
	 * @return $this
	 */
	public function on($c1, $op, $c2) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'on',
				'args' => array (
						strpos($c1, ".")===false?$this->_table_name.".".$c1:$c1,
						$op,
						strpos($c2, ".")===false?$this->_table_name.".".$c2:$c2,
				)
		);

		return $this;
	}

	/**
	 * Creates a "GROUP BY ..." filter.
	 *
	 * @param mixed $columns
	 *        	column name
	 * @param
	 *        	...
	 * @return $this
	 */
	public function group_by($columns) {
		// Add pending database call which is executed after query type is determined
		$columns=strpos($columns, ".")===false?$this->_table_name.".".$columns:$columns;
		$this->_db_pending [] = array (
				'name' => 'group_by',
				'args' => $columns
		);

		return $this;
	}

	/**
	 * Alias of and_having()
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function having($column, $op, $value = NULL) {
		return $this->and_having ( $column, $op, $value );
	}

	/**
	 * Creates a new "AND HAVING" condition for the query.
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function and_having($column, $op, $value = NULL) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_having',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$op,
						$value
				)
		);

		return $this;
	}

	/**
	 * Creates a new "OR HAVING" condition for the query.
	 *
	 * @param mixed $column
	 *        	column name or array($column, $alias) or object
	 * @param string $op
	 *        	logic operator
	 * @param mixed $value
	 *        	column value
	 * @return $this
	 */
	public function or_having($column, $op, $value = NULL) {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_having',
				'args' => array (
						strpos($column, ".")===false?$this->_table_name.".".$column:$column,
						$op,
						$value
				)
		);

		return $this;
	}

	/**
	 * Alias of and_having_open()
	 *
	 * @return $this
	 */
	public function having_open() {
		return $this->and_having_open ();
	}

	/**
	 * Opens a new "AND HAVING (...)" grouping.
	 *
	 * @return $this
	 */
	public function and_having_open() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_having_open',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Opens a new "OR HAVING (...)" grouping.
	 *
	 * @return $this
	 */
	public function or_having_open() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_having_open',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Closes an open "AND HAVING (...)" grouping.
	 *
	 * @return $this
	 */
	public function having_close() {
		return $this->and_having_close ();
	}

	/**
	 * Closes an open "AND HAVING (...)" grouping.
	 *
	 * @return $this
	 */
	public function and_having_close() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'and_having_close',
				'args' => array ()
		);

		return $this;
	}

	/**
	 * Closes an open "OR HAVING (...)" grouping.
	 *
	 * @return $this
	 */
	public function or_having_close() {
		// Add pending database call which is executed after query type is determined
		$this->_db_pending [] = array (
				'name' => 'or_having_close',
				'args' => array ()
		);

		return $this;
	}
	/**
	 * set transaction and start transaction
	 * @param Transaction $transaction
	 * @return \LSYS\ORM
	 */
	public function set_transaction(Transaction $transaction){
		$transaction->add_transaction($this->_db);
		return $this;
	}
}